/*
 * application_commons.h
 *
 *  Created on: Dec 15, 2012
 *      Author: m
 */

#ifndef APPLICATION_COMMONS_H_
#define APPLICATION_COMMONS_H_

#include <sys/time.h>
#include <iostream>
#include <cstring>
#include <sstream>
#include "../physical_layer/file_system/PhraseDictionary.h"
#include "../physical_layer/file_system/Phrase.h"

using std::string;
//using physical::Phrase;

#define USAGE_ARG  "-usage"
#define HELP_ARG   "-help"
#define BUILD_CMD  "-construir"
#define ADD_CMD    "-agregar"
#define UPDATE_CMD "-modificar"
#define REMOVE_CMD "-eliminar"
#define QUERY_CMD  "-consultar"
#define PHRASE_TEXT_ARG    "-texto"
#define AUTHOR_SURNAME_ARG "-apellido"
#define AUTHOR_NAME_ARG    "-nombre"

/**
 * Modos de funcionamiento de la aplicación
 */
typedef enum { BUILD, ADD, UPDATE, REMOVE, QUERY, INVALID } system_mode;

/**
 * Estructura que contiene las opciones parseadas de los argumentos
 */
typedef struct
{
    system_mode mode;
    string newText;
    string newAuthorSurname;
    string newAuthorName;
    size_t phraseId;
    string query;
} options_t;

/**
 * Función que imprime la explicación de como usarla aplicación
 */
void print_usage()
{
    std::cerr
        <<  std::endl
        <<  "Uso: programa COMANDO [OPCIONES]" << std::endl
        <<  std::endl
        <<  std::endl
        <<  "Comandos:" << std::endl
        <<  std::endl
        <<  " -help : Este mensaje de ayuda" << std::endl
        <<  std::endl
        <<  " -usage : idem -help" << std::endl
        <<  std::endl
        <<  " -construir :" << std::endl
        <<  "     Construye un nuevo índice a partir del diccionario" << std::endl
        <<  "      de frases ( creado con la aplicación phraseDictionary )," << std::endl
        <<  "      cualquier información previa que tenga el índice es eliminada." << std::endl
        <<  "      Nota: Tener en cuenta que el nombre del archivo de diccionario " << std::endl
        <<  "             de frases está fijo ( tipo_de_indice.phrasedictionary.dat )." << std::endl
        <<  std::endl
        <<  " -agregar <sub-comandos> :" << std::endl
        <<  "     Agrega una nueva frase al diccionario y la indexa." << std::endl
        <<  std::endl
        <<  "     Los sub-comandos son:" << std::endl
        <<  "         -texto \"nueva frase\": Texto de la frase a cargar" << std::endl
        <<  "         -nombre \"nombre del autor\"" << std::endl
        <<  "         -apellido \"apellido del autor\"" << std::endl
        <<  std::endl
        <<  "     Nota: el subcomando -texto es obligatorio! Los demás son opcionales" << std::endl
        <<  std::endl
        <<  " -modificar idFrase <sub-comandos> :" << std::endl
        <<  "     Modifica la frase existente con id = idFrase" << std::endl
        <<  std::endl
        <<  "     Los sub-comandos son:" << std::endl
        <<  "         -texto \"nueva frase\": Texto de la frase a cargar" << std::endl
        <<  "         -nombre \"nombre del autor\"" << std::endl
        <<  "         -apellido \"apellido del autor\"" << std::endl
        <<  std::endl
        <<  "     Nota: Al menos un sub-comando debe ser ingresado." << std::endl
        <<  std::endl
        <<  " -eliminar idFrase:" << std::endl
        <<  "     Elimina la frase existente con id = idFrase." << std::endl
        <<  std::endl
        <<  " -consultar \"consulta\":" << std::endl
        <<  "     Devuelve todos los resultados que tengan un match con la consulta." << std::endl
        <<  "     El formato para realizar consultas es el siguiente:" << std::endl
        <<  "         AND: \"termino1 & termino2 & termino3 ...\"" << std::endl
        <<  "         OR:  \"termino1 | termino2 | termino3 ...\"" << std::endl
        <<  "         NOT: \"~termino1 ~termino2 ...\"" << std::endl
        <<  " Nota: Las stop-words se ignoran en las consultas, por lo tanto una" << std::endl
        <<  "        consulta compuesta completamente por stop-words se considera" << std::endl
        <<  "        una consulta vacia." << std::endl;
}

/**
 * Función que imprime el mensaje de argumentos inválidos y
 *  termina el programa.
 */
void invalid_args()
{
    std::cerr << "Argumentos inválidos." << std::endl;
    print_usage();
    exit(1);
}

/**
 * Función que parsea los argumentos
 *
 * @param argc
 * @param argv
 * @param options
 */
void parse_arguments( int argc, const char *argv[], options_t &options )
{
    options.mode = system_mode::INVALID;

    for ( int i = 1; i < argc; ++i )
    {

        if ( strcmp( argv[i], USAGE_ARG ) == 0 || strcmp( argv[i], HELP_ARG ) == 0 )
        {
            print_usage();
            exit(0);
        }

        if ( strcmp( argv[i], BUILD_CMD ) == 0 )
        {
            if ( options.mode != system_mode::INVALID )
            {
                invalid_args();
            }

            options.mode = system_mode::BUILD;
            continue;
        }

        if ( strcmp( argv[i], ADD_CMD ) == 0 )
        {
            if ( options.mode != system_mode::INVALID )
            {
                invalid_args();
            }

            options.mode = system_mode::ADD;

            int j = 1;
            for ( ; j < 6 && j + i < argc; ++j )
            {
                if ( strcmp( argv[ i + j ], PHRASE_TEXT_ARG ) == 0 )
                {
                    if ( j + i == argc - 1 )
                    {
                        invalid_args();
                    }

                    options.newText = argv[ i + j + 1 ];
                    j++;
                    continue;
                }

                if ( strcmp( argv[ i + j ], AUTHOR_SURNAME_ARG ) == 0 )
                {
                    if ( j + i == argc - 1 )
                    {
                        invalid_args();
                    }

                    options.newAuthorSurname = argv[ i + j + 1 ];
                    j++;
                    continue;
                }

                if ( strcmp( argv[ i + j ], AUTHOR_NAME_ARG ) == 0 )
                {
                    if ( j + i == argc - 1 )
                    {
                        invalid_args();
                    }

                    options.newAuthorName = argv[ i + j + 1 ];
                    j++;
                    continue;
                }
            }

            if ( options.newText.empty() )
            {
                invalid_args();
            }

            i += j - 1;
            continue;
        }

        if ( strcmp( argv[i], UPDATE_CMD ) == 0 )
        {
            if ( options.mode != system_mode::INVALID )
            {
                invalid_args();
            }

            if ( i == argc - 1 )
            {
                invalid_args();
            }

            options.mode = system_mode::UPDATE;
            options.phraseId = atoi( argv[ i + 1 ] );

            if ( options.phraseId == 0 )
            {
                invalid_args();
            }

            i++;

            int j = 1;
            for ( ; j < 6 && j + i < argc; ++j )
            {
                if ( strcmp( argv[ i + j ], PHRASE_TEXT_ARG ) == 0 )
                {
                    if ( j + i == argc - 1 )
                    {
                        invalid_args();
                    }

                    options.newText = argv[ i + j + 1 ];
                    j++;
                    continue;
                }

                if ( strcmp( argv[ i + j ], AUTHOR_SURNAME_ARG ) == 0 )
                {
                    if ( j + i == argc - 1 )
                    {
                        invalid_args();
                    }

                    options.newAuthorSurname = argv[ i + j + 1 ];
                    j++;
                    continue;
                }

                if ( strcmp( argv[ i + j ], AUTHOR_NAME_ARG ) == 0 )
                {
                    if ( j + i == argc - 1 )
                    {
                        invalid_args();
                    }

                    options.newAuthorName = argv[ i + j + 1 ];
                    j++;
                    continue;
                }
            }

            i += j - 1;
            continue;
        }

        if ( strcmp( argv[i], REMOVE_CMD ) == 0 )
        {
            if ( options.mode != system_mode::INVALID )
            {
                invalid_args();
            }

            if ( i == argc - 1 )
            {
                invalid_args();
            }

            options.mode = system_mode::REMOVE;
            options.phraseId = atoi( argv[ i + 1 ] );

            if ( options.phraseId == 0 )
            {
                invalid_args();
            }

            i++;
            continue;
        }

        if ( strcmp( argv[i], QUERY_CMD ) == 0 )
        {
            if ( options.mode != system_mode::INVALID )
            {
                invalid_args();
            }

            if ( i == argc - 1 )
            {
                invalid_args();
            }

            options.mode = system_mode::QUERY;
            options.query = argv[ i + 1 ];

            i++;
            continue;
        }
    }
}



/**
 *  Esta clase es un diccionario de stop-words
 *  Se utiliza para filtrar stop-words
 */
class StopWordsDictionary
{
    std::list<std::string> stopWords;

public:
    void load( std::ifstream &ifs )
    {
        std::string lineBuffer;

        while ( getline( ifs, lineBuffer ) )
        {
            stopWords.push_back( lineBuffer );
        }
    }

    bool isStopWord(std::string &str)
    {
        for ( auto it = stopWords.begin(); it != stopWords.end(); it++)
        {
            if (it->compare(str) == 0)
            {
                return true;
            }
        }

        return false;
    }
} stopWordsDictionary;

/**
 * Estructura invalid symbol
 * Representa un caracter inválido (tildes y demás)
 *  y tiene un posible reemplazo para volverlo valido
 */
struct invalidSymbol
{
    std::string m_symbol;
    std::string m_replacement;

    invalidSymbol( std::string const &symbol, std::string const &replacement ) :
            m_symbol(symbol), m_replacement(replacement)
    {
    }
};

/**
 * Diccionario de simbolos "extraños" y sus reemplazos
 */
class InvalidSymbolsDictionary
{
private:
    typedef std::list<invalidSymbol> invalidSymbolsCollection;

public:
    typedef invalidSymbolsCollection::iterator iterator;

public:
    InvalidSymbolsDictionary()
    {
        initialize();
    }

    iterator begin()
    {
        return invalidSymbols.begin();
    }

    iterator end()
    {
        return invalidSymbols.end();
    }

private:
    invalidSymbolsCollection invalidSymbols;

    void initialize()
    {
        invalidSymbols.push_back( invalidSymbol( "á", "a" ) );
        invalidSymbols.push_back( invalidSymbol( "à", "a" ) );
        invalidSymbols.push_back( invalidSymbol( "â", "a" ) );
        invalidSymbols.push_back( invalidSymbol( "ä", "a" ) );

        invalidSymbols.push_back( invalidSymbol( "é", "e" ) );
        invalidSymbols.push_back( invalidSymbol( "è", "e" ) );
        invalidSymbols.push_back( invalidSymbol( "ê", "e" ) );
        invalidSymbols.push_back( invalidSymbol( "ë", "e" ) );

        invalidSymbols.push_back( invalidSymbol( "í", "i" ) );
        invalidSymbols.push_back( invalidSymbol( "ì", "i" ) );
        invalidSymbols.push_back( invalidSymbol( "î", "i" ) );
        invalidSymbols.push_back( invalidSymbol( "ï", "i" ) );

        invalidSymbols.push_back( invalidSymbol( "ó", "o" ) );
        invalidSymbols.push_back( invalidSymbol( "ò", "o" ) );
        invalidSymbols.push_back( invalidSymbol( "ô", "o" ) );
        invalidSymbols.push_back( invalidSymbol( "ö", "o" ) );

        invalidSymbols.push_back( invalidSymbol( "ú", "u" ) );
        invalidSymbols.push_back( invalidSymbol( "ù", "u" ) );
        invalidSymbols.push_back( invalidSymbol( "û", "u" ) );
        invalidSymbols.push_back( invalidSymbol( "ü", "u" ) );

        invalidSymbols.push_back( invalidSymbol( ",", " " ) );
        invalidSymbols.push_back( invalidSymbol( ".", " " ) );
        invalidSymbols.push_back( invalidSymbol( ";", " " ) );
        invalidSymbols.push_back( invalidSymbol( ":", " " ) );
        invalidSymbols.push_back( invalidSymbol( "?", " " ) );
        invalidSymbols.push_back( invalidSymbol( "|", " " ) );
        invalidSymbols.push_back( invalidSymbol( "¿", " " ) );
        invalidSymbols.push_back( invalidSymbol( "!", " " ) );
        invalidSymbols.push_back( invalidSymbol( "¡", " " ) );
        invalidSymbols.push_back( invalidSymbol( "[", " " ) );
        invalidSymbols.push_back( invalidSymbol( "]", " " ) );
        invalidSymbols.push_back( invalidSymbol( "(", " " ) );
        invalidSymbols.push_back( invalidSymbol( ")", " " ) );
        invalidSymbols.push_back( invalidSymbol( "<", " " ) );
        invalidSymbols.push_back( invalidSymbol( ">", " " ) );
        invalidSymbols.push_back( invalidSymbol( "'", " " ) );
        invalidSymbols.push_back( invalidSymbol( "/", " " ) );
        invalidSymbols.push_back( invalidSymbol( "-", " " ) );
        invalidSymbols.push_back( invalidSymbol( "~", " " ) );
        invalidSymbols.push_back( invalidSymbol( "$", " " ) );
        invalidSymbols.push_back( invalidSymbol( "%", " " ) );
        invalidSymbols.push_back( invalidSymbol( "#", " " ) );
        invalidSymbols.push_back( invalidSymbol( "@", " " ) );
        invalidSymbols.push_back( invalidSymbol( "º", " " ) );
        invalidSymbols.push_back( invalidSymbol( "ª", " " ) );
        invalidSymbols.push_back( invalidSymbol( "*", " " ) );
        invalidSymbols.push_back( invalidSymbol( "+", " " ) );
        invalidSymbols.push_back( invalidSymbol( "^", " " ) );
        invalidSymbols.push_back( invalidSymbol( "¬", " " ) );
        invalidSymbols.push_back( invalidSymbol( "½", " " ) );
        invalidSymbols.push_back( invalidSymbol( "¨", " " ) );
        invalidSymbols.push_back( invalidSymbol( "…", " " ) );
        invalidSymbols.push_back( invalidSymbol( "\"", " " ) );
        invalidSymbols.push_back( invalidSymbol( "\\", " " ) );
    }
} invalidSymbolsDictionary;

/**
 * Función que reemplaza cada caracter considerado "simbolo extraño"
 *  por un "equivalente aceptable"
 *
 * @param str
 */
void replaceSymbols(std::string &str)
{
    auto it = invalidSymbolsDictionary.begin();

    for ( ; it != invalidSymbolsDictionary.end(); it++ )
    {
        size_t pos = str.find( it->m_symbol );
        while ( pos != std::string::npos )
        {
            str.replace( pos, it->m_symbol.size(), it->m_replacement );
            pos = str.find( it->m_symbol );
        }
    }
}

/**
 *  Función personalizada que pasa un string a minúsculas
 *  Nota: es personalizada porque tolower parece que rompia las ñ
 *
 * @param str
 */
void toLower( std::string &str )
{
    std::string upper_enie( "Ñ" );
    std::string lower_enie( "ñ" );

    // reemplazo las Ñ por ñ
    size_t pos = str.find( upper_enie );

    while ( pos != std::string::npos )
    {
        str.replace( pos, upper_enie.size(), lower_enie );
        pos = str.find( upper_enie );
    }

    // ahora aplico tolower a las demás letras
    for ( auto it = str.begin(); it != str.end(); it++ )
    {
        if ( *it <= 'Z' && *it >= 'A' )
            *it -= 'Z' - 'z';
    }
}

/**
 * Carga un Diccionario de stop-words de un archivo de texto
 *
 * @param stopWordsDictionary El diccionario donde se almacenan las stop-words
 */
void loadStopWords( const char* stopWordsFile, StopWordsDictionary &stopWordsDictionary )
{
    std::ifstream input( stopWordsFile );

    stopWordsDictionary.load( input );
}

/**
 * Función encargada de realizar la normalización de palabras
 *
 * @param str String a normalizar
 * @return El String normalizado
 */
std::string normalize( std::string str )
{
    std::string normalizedStr( str );

    // paso las letras de la palabra a minúsculas
    toLower( normalizedStr );

    // realizo reemplazos de tildes y otros símbolos
    replaceSymbols( normalizedStr );

    return normalizedStr;
}

/**
 * Función que extre los términos relevantes de una frase, los
 *  normaliza (sanitize + lower case) y los devuelve en una lista
 *
 * @param phrase Frase de la que se desean extraer los términos
 * @return Lista de Términos relevantes normalizados
 */
std::list< std::string > extractRelevantTerms( const physical::Phrase &phrase )
{
    std::list< std::string > result;
    std::string fullPhrase;
    std::string normalizedPhrase;
    std::string term;

    /*
     * Creo una oración con el texto de la frase y la
     *  información del autor
     */
    fullPhrase.append( phrase.getText() );
    fullPhrase.append(" ");
    fullPhrase.append( phrase.getAuthorName() );
    fullPhrase.append(" ");
    fullPhrase.append( phrase.getAuthorSurname() );

    // Normalizo la frase completa
    normalizedPhrase = normalize( fullPhrase );

    std::stringstream ss( normalizedPhrase );

    /*
     * Extraigo las palabras de a una
     *  Si son stop-words las descarto
     *  Sino las agrego a la lista de resultados
     */
    while ( ss >> term )
    {
        // Verifico que no esté vacia
        if ( term.empty() )
            continue;

        // verificar que no sea stop-word
        if ( stopWordsDictionary.isStopWord( term ) )
            continue;

        result.push_back( term );
    }

    // Ordeno y elimino duplicados
    result.sort();
    result.unique();

    return result;
}

/**
 * Tipo de consulta
 */
typedef enum { SIMPLE_QUERY, AND_QUERY, OR_QUERY, NOT_QUERY, INVALID_QUERY } query_type;

/**
 * Estructura que representa una consulta
 */
struct query
{
    query_type queryType;
    std::list < std::string > termList;
};

/**
 * Lista de operadores de consultas
 */
std::list < std::string > queryOperators( { "&", "|", "~" } );

/**
 * Función que parsea una consulta de un string
 * @param queryString
 * @return
 */
query parseQuery( const std::string &str )
{
    query result;
    std::string term;
    std::string operatorFound;
    std::string sanitizedQuery( str );

    result.queryType = query_type::INVALID_QUERY;

    // Reemplazo los operadores con espacios
    for ( auto it = queryOperators.begin(); it != queryOperators.end(); it++ )
    {
        size_t pos = sanitizedQuery.find( *it );
        while ( pos != std::string::npos )
        {
            operatorFound = *it;
            sanitizedQuery.replace( pos, it->size(), " " );
            pos = sanitizedQuery.find( *it );
        }
    }

    // Defino el tipo de consulta en base al operador encontrado
    if ( operatorFound == "&" )
    {
        result.queryType = query_type::AND_QUERY;
    }
    if ( operatorFound == "|" )
    {
        result.queryType = query_type::OR_QUERY;
    }
    if ( operatorFound == "~" )
    {
        result.queryType = query_type::NOT_QUERY;
    }

    // Normalizo la query completa
    std::stringstream ss( normalize( sanitizedQuery ) );

    while ( ss >> term )
    {
        // Verifico que no esté vacia
        if ( term.empty() )
            continue;

        // verificar que no sea stop-word
        if ( stopWordsDictionary.isStopWord( term ) )
            continue;

        // Agrego el término de la consulta
        result.termList.push_back( term );
    }

    // Ordeno y elimino duplicados
    result.termList.sort();
    result.termList.unique();

    if ( result.termList.size() == 1 && result.queryType != query_type::NOT_QUERY )
    {
        result.queryType = query_type::SIMPLE_QUERY;
    }

    return result;
}

void invalid_query()
{
    std::cerr << "Consulta inválida, usar -help o -usage para más información." << std::endl;
    exit(21);
}

unsigned long long getTimeInMicroSeconds()
{
    struct timeval tv;
    gettimeofday(&tv, NULL);

    return ((((unsigned long long)tv.tv_sec) * 1000000) +
        (((unsigned long long)tv.tv_usec) ));
}

#endif /* APPLICATION_COMMONS_H_ */
